package net.cloudcodex.server.service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import net.cloudcodex.server.Context;
import net.cloudcodex.server.data.Data;
import net.cloudcodex.server.data.Data.Campaign;
import net.cloudcodex.server.data.Data.Message;
import net.cloudcodex.server.data.Data.Scene;
import net.cloudcodex.server.data.Data.User;
import net.cloudcodex.server.data.campaign.msg.SceneSDO;
import net.cloudcodex.server.data.campaign.scene.SceneToCreateSDO;
import net.cloudcodex.shared.Errors;
import net.cloudcodex.shared.MessageAction;
import net.cloudcodex.shared.MessageType;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Transaction;

/**
 * Service for Messages
 * @author Thomas
 */
public class MessageService extends AbstractCampaignService {

	/**
	 * @param store Google AppEngine datastore.
	 */
	public MessageService(DatastoreService store) {
		super(store);
	}

	/**
	 * Create a scene.
	 * TODO : utility, replace with a complete scenes organization method.
	 * @param introduction introduction 
	 * @param characters characters to join.
	 * @return the newly created scene.
	 */
	public Scene startScene(Context context, String introduction, Data.Character... chars) {

		final List<Data.Character> characters = new ArrayList<Data.Character>(Arrays.asList(chars));
		
		final Key campaignKey = characters.get(0).getKey().getParent();

		final Set<Key> oldseqsKeys = new LinkedHashSet<Key>();
		final Map<Key, List<Data.Character>> charactersByScene = 
			new HashMap<Key, List<Data.Character>>();
		
		// check characters validity
		for(Data.Character character : characters) {
			final Key characterKey = character.getKey();
			if(character == null || !characterKey.getParent().equals(campaignKey)) {
				logger.severe("startScene() : not of the same campaign : " + characterKey);
				return null;
			}
			if(character.getOwner() != null) { // !NPC
				final Key sceneKey = character.getScene();
				
				if(sceneKey != null) {
					// keep trace of distinct old scenes
					oldseqsKeys.add(sceneKey);

					// sort characters by old scene.
					List<Data.Character> seqChars = 
						charactersByScene.get(sceneKey);
					if(seqChars == null ){
						seqChars = new ArrayList<Data.Character>();
						charactersByScene.put(sceneKey, seqChars);
					}
					seqChars.add(character);
				}
			}
		}
		
		final Campaign campaign = dao.readCampaign(context, campaignKey);
		
		if(campaign == null) {
			logger.severe("startScene() : campaign not found :" + campaignKey);
			context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
			return null;
		}

		final List<Scene> oldseqs = dao.readScenes(context, oldseqsKeys);
		
		// Start a TX, all entities remains to Campaign	
		final Transaction tx = dao.getStore().beginTransaction();
		try {
			if(oldseqs != null) {
				for(Scene oldseq : oldseqs) {
					
					// get characters where current scene is oldseq
					final List<Data.Character> others = getSceneCharacters(context, oldseq.getKey());
					
					// Remove the characters for the new scene.
					dao.removeAll(others, characters);

					// Keep only playable characters
					final List<Data.Character> otherPCs = removeNPCs(others);

					if(otherPCs != null && !otherPCs.isEmpty()) {

						// ... create a new alternative scene for "the others" ...
						final Scene newseq = new Scene(campaign);
						newseq.setDate(new Date());
						newseq.setCharacters(dao.toKeysFromData(others)); // includes NPC

						// ... link it to the odlseq for each PC ...
						for(Data.Character otherPC : otherPCs) {
							final String index = String.valueOf(otherPC.getKey().getId());
							newseq.setPrevious(index, oldseq.getKey());
						}

						dao.save(context, newseq);
						logger.info(toStringKeys(others) + " go to " + newseq.getKey() + " alternative scene");
						
						// ... and link the oldseq to this new alternative scene
						// In 2 steps because newseq.key was not set before
						for(Data.Character otherPC : otherPCs) {
							final String index = String.valueOf(otherPC.getKey().getId());
							oldseq.setNext(index, newseq.getKey());
							
							// save the new alternative scene as current "other characters"'s scene
							otherPC.setScene(newseq);
							dao.save(context, otherPC);
						}
					} else {
						logger.info("there was no others playable characters");
					}
				}
			}

			// create the new scene with characters and link to old scenes.
			final Scene newseq = new Scene(campaign);
			newseq.setDate(new Date());
			newseq.setIntroduction(introduction);
			newseq.setCharacters(dao.toKeysFromData(characters)); // includes NPCs
			for(Data.Character character : characters) {
				if(character.getOwner() != null) { // !NPC
					final String index = String.valueOf(character.getKey().getId());
					newseq.setPrevious(index, character.getScene());
				}
			}
			dao.save(context, newseq);
			
			// create the forward link from old scenes to the new scene.
			if(oldseqs != null) {
				for(Scene oldseq : oldseqs) {
					final List<Data.Character> oldseqChars = 
						charactersByScene.get(oldseq.getKey());
					if(oldseqChars != null) {
						for(Data.Character oldseqChar : oldseqChars) {
							// here we iterate only on PC associated to this old scene
							final String index = String.valueOf(oldseqChar.getKey().getId());
							oldseq.setNext(index, newseq.getKey());
						}
					}

					// useless but ... for the future ?
					oldseq.setClosed(true); 
					
					dao.save(context, oldseq);
				}				
			}
			
			// save the new scene as current characters's scene
			for(Data.Character character : characters) {
				if(character.getOwner() != null) { // !NPC
					character.setScene(newseq);
					dao.save(context, character);
				}
			}
			
			tx.commit();

			logger.info("scene " + newseq.getKey() + " created for " + toStringKeys(characters));
			return newseq;
			
		} finally {
			if(tx.isActive()) {
				tx.rollback();
			}
		}
	}


	
	/**
	 * Starts a scene for the specified characters.
	 * TODO : utility, replace with a complete scenes organization method.
	 * @param introduction Introduction text of the the scene.
	 * @param charactersKeys characters associated with the new scene.
	 * @return the new scene.
	 */
	public Scene startScene(Context context, String introduction, Key... charactersKeys) {

		if(charactersKeys == null || charactersKeys.length == 0) {
			logger.severe("startScene() : no characters");
			context.addError(Errors.REQUIRED, "characters");
			return null;
		}

		final List<Data.Character> characters = 
			dao.readCharacters(context, Arrays.asList(charactersKeys));

		if(characters == null || characters.isEmpty()) {
			logger.severe("startScene() : characters not found");
			context.addError(Errors.NOT_FOUND_CHARACTER);
			return null;
		}

		return startScene(context, introduction, 
				characters.toArray(new Data.Character[characters.size()]));
	}
	
	public boolean startScenes(Context context, long campaignId, SceneToCreateSDO[] scenes) {
		
		if(scenes == null || scenes.length == 0) {
			logger.severe("no scene");
			context.addError(Errors.REQUIRED, "scenes");
			return false;
		}
		
		final Key campaignKey = Campaign.createKey(campaignId);
		final Campaign campaign = dao.readCampaign(context, campaignKey);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignKey);
			context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
			return false;
		}
		
		// Create a list of characters and pcs
		final Map<Key, Data.Character> pcs = new HashMap<Key, Data.Character>();
		final Map<Key, Data.Character> allCharacters = new HashMap<Key, Data.Character>();
		final Map<SceneToCreateSDO, List<Data.Character>> scenesCharacters = 
				new HashMap<SceneToCreateSDO, List<Data.Character>>();
		final Map<SceneToCreateSDO, List<Key>> scenesCharactersKeys = 
				new HashMap<SceneToCreateSDO, List<Key>>();
		
		for(SceneToCreateSDO scene : scenes) {
			final long[] charactersId = scene.getCharacters();
			if(charactersId == null || charactersId.length == 0) {
				logger.severe("no characters");
				context.addError(Errors.REQUIRED, "characters");
				return false;
			}

			// count the PCS
			int scenePCs = 0;
			
			final List<Data.Character> sceneCharacters = new ArrayList<Data.Character>();
			final List<Key> sceneCharactersKeys = new ArrayList<Key>();
			
			// load and filter the characters
			for(long characterId : charactersId) {
				final Key characterKey = 
						Data.Character.createKey(campaignKey, characterId);

				// check the character is already found as PC in another scene
				if(pcs.containsKey(characterKey)) {
					logger.severe("character " + characterKey + " in multiple scenes");
					context.addError(Errors.IMPOSSIBLE_UBIQUITY, characterKey);
					return false;
				}
				
				final Data.Character character = dao.readCharacter(context, characterKey);
				if(character == null) {
					logger.severe("invalid character " + characterKey);
					context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
					return false;
				}
				
				if(character.getOwner() != null) {
					pcs.put(character.getKey(), character);
					scenePCs++;
				}
				allCharacters.put(character.getKey(), character);
				sceneCharacters.add(character);
				sceneCharactersKeys.add(characterKey);
			}
			
			// just to get them easily after
			scenesCharacters.put(scene, sceneCharacters);
			scenesCharactersKeys.put(scene, sceneCharactersKeys);
			
			if(scenePCs == 0) {
				logger.severe("try to create a scene with only NPCs");
				context.addError(Errors.IMPOSSIBLE_ONLY_NPCS);
				return false;
			}
		}
		
		// a way to remember the PCs last scene
		final Map<Key, Scene> lastScenes = new HashMap<Key, Scene>();
		
		// Create a list of PCs's current scenes
		final List<Scene> currentScenes = new ArrayList<Scene>();
		for(Data.Character pc : pcs.values()) {
			final Key sceneKey = pc.getScene();
			if(sceneKey != null) {
				final Scene currentScene = dao.readScene(context, sceneKey);
				if(currentScene == null) {
					logger.severe("invalid scene " + sceneKey);
					context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
					return false;
				}
				
				lastScenes.put(pc.getKey(), currentScene);
				
				if(!currentScenes.contains(currentScene)) {
					currentScenes.add(currentScene);
					
					// check the scene doesn't contains another PC not listed
					if(currentScene.getCharacters() != null) {
						for(Key characterKey : currentScene.getCharacters()) {
							if(!allCharacters.containsKey(characterKey)) {
								final Data.Character character = 
									dao.readCharacter(context, characterKey);
								if(character == null) {
									logger.severe("invalid character " + characterKey);
								} else {
									if(character.getOwner() != null) {
										logger.severe("try to create a scene but " 
												+ characterKey + " not dispatched");
										context.addError(Errors.IMPOSSIBLE_PC_NOT_DISPATCHED, 
												characterKey);
										return false;
									}
								}
							}
						}
					}
				}
			}
		}
		
		// At this points all PCs of impacted scenes are listed and used 
		// only one time, we are sure ... so create the new scenes

		// Start a TX, all entities remains to Campaign	
		final Transaction tx = dao.getStore().beginTransaction();
		try {
			for(SceneToCreateSDO sceneToCreate : scenes) {
				final List<Key> charactersKeys = scenesCharactersKeys.get(sceneToCreate);
				final List<Data.Character> characters = scenesCharacters.get(sceneToCreate);
				
				final Scene scene = new Scene(campaign);
				scene.setDate(new Date());
				scene.setCharacters(charactersKeys);
				scene.setIntroduction(sceneToCreate.getIntroduction());

				final Map<Long, Map<Long, String>> allAliases = sceneToCreate.getAliases();
				for(Data.Character character : characters) {
					final Key characterKey = character.getKey();

					// set the aliases
					if(allAliases != null) {
						final Map<Long, String> charAliases = allAliases.get(characterKey.getId());
						if(charAliases != null) {
							for(Map.Entry<Long, String> entry : charAliases.entrySet()) {
								final Long charId = entry.getKey();
								final String alias = entry.getValue();
								if(charId == null) {
									// global alias
									scene.setAlias(String.valueOf(characterKey.getId()), alias);
								} else {
									// specific alias
									scene.setAlias(String.valueOf(characterKey.getId()) 
											+ "-" + String.valueOf(charId), alias);
								}
							}
						}
					}
				}

				// set the "previous" property of the new scene, for each PC
				for(Data.Character character : characters) {
					if(character.getOwner() != null) {
						final Key characterKey = character.getKey();
						final Scene lastScene = lastScenes.get(characterKey);
						if(lastScene != null) {
							scene.setPrevious(String.valueOf(characterKey.getId()), lastScene.getKey());
						}
					}
				}
				
				// after that we have a scene key
				dao.save(context, scene);

				// update the PCs and the last scene
				final List<Scene> updatedScenes = new ArrayList<Scene>();
				for(Data.Character character : characters) {
					if(character.getOwner() != null) {

						// set the PC current scene
						character.setScene(scene.getKey());
						dao.save(context, character);
						
						// set the last scene's "next" scene.
						final Key characterKey = character.getKey();
						final Scene lastScene = lastScenes.get(characterKey);
						if(lastScene != null) {
							lastScene.setNext(String.valueOf(characterKey.getId()), scene.getKey());
							if(!updatedScenes.contains(lastScene)) {
								updatedScenes.add(lastScene);
							}
						}
					}
				}
				
				// done after to update each scene only one time
				for(Scene updatedScene : updatedScenes) {
					dao.save(context, updatedScene);
				}
			}
			
			tx.commit();
		} finally {
			if(tx.isActive()) {
				tx.rollback();
			}
		}
		return true;
	}
	
	
	
	
	/**
	 * Post a Speech Message.
	 * @param sceneKey scene containing the message.
	 * @param character character who posts the speech.
	 * @param text text of the speech.
	 * @return true if ok.
	 */
	public boolean postSpeech(Context context, Key sceneKey, Data.Character character, String text) {

		if(character.getOwner() != null) { // !NPC
			// NPC can post everywhere ... GM is a god !
			// But sceneKey must be specified when NPCs post.
			sceneKey = character.getScene();
		}

		if(sceneKey == null) {
			return false;
		}

		final Scene scene = dao.readScene(context, sceneKey);
		
		if(scene == null) {
			return false;
		}

		// FIXME should iterate to deal with concurrent updates of scene
		final Message message = new Message(scene);
		message.setAuthor(character);
		message.setContent(text);
		message.setType(MessageType.ACTION.getCode());
		message.setAction(MessageAction.SPEECH.getCode());
		createMessage(context, scene, message);
		return true;
	}
	
	
	/**
	 * To post an action message by character.
	 * 
	 * @param context execution context.
	 * @param campaignId campaign's id.
	 * @param characterId character's id.
	 * @param clientSceneId scene where post message, used to check client is up to date.
	 * @param clientSceneTimestamp just to check if the client is up to date.
	 * @param action action.
	 * @param content message content.
	 * @return <code>true</code> if ok.
	 */
	public boolean playerPostAction(Context context, 
			long campaignId, long characterId, long clientSceneId, 
			Date clientSceneTimestamp, MessageAction action, String content) {

		return playerPostAction(context, 
				createCharacterKey(campaignId, characterId), 
				createSceneKey(campaignId, clientSceneId), 
				clientSceneTimestamp, action, content);
	}
	
	
	/**
	 * To post an action message by character.
	 * 
	 * @param context execution context.
	 * @param characterKey character's key.
	 * @param clientSceneKey scene where post message, used to check client is up to date.
	 * @param clientSceneTimestamp just to check if the client is up to date.
	 * @param action action.
	 * @param content message content.
	 * @return <code>true</code> if ok.
	 */
	public boolean playerPostAction(Context context, 
			Key characterKey, Key clientSceneKey, Date clientSceneTimestamp,
			MessageAction action, String content) {

		if(action == null || characterKey == null 
				|| content == null || clientSceneKey == null
				|| clientSceneTimestamp == null) {
			logger.severe("missing param");
			context.addError(Errors.REQUIRED);
			return false;
		}
		
		// check the character.
		final Data.Character character = dao.readCharacter(context, characterKey);
		if(character == null) {
			logger.severe("invalid character " + characterKey);
			context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
			return false;
		}
		
		// check character's lock
		if(Boolean.TRUE.equals(character.getLocked())) {
			logger.severe("Character " + characterKey + " is locked");
			context.addError(Errors.IMPOSSIBLE_LOCKED, characterKey);
			return false;
		}
		
		// check character is not dead
		if(Boolean.TRUE.equals(character.getDead())) {
			logger.severe("Character " + characterKey + " is dead");
			context.addError(Errors.IMPOSSIBLE_DEAD, characterKey);
			return false;
		}		
		// check user rights
		if(!isOwner(context, character)) {
			logger.severe(context.getUser().getKey() 
					+ " cannot post for " + characterKey);
			context.addError(Errors.USER_USURPATION_PC);
			return false;
		}

		// Get the scene.
		final Key sceneKey = character.getScene();
		
		if(sceneKey == null) {
			logger.severe(characterKey + " cannot post, it has no current scene");
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		// Check the client is up-to-date
		if(!sceneKey.equals(clientSceneKey)) {
			logger.severe("client is out-of-date");
			context.addError(Errors.OUTOFDATE);
			return false;
		}

		// check the scene
		final Scene scene = dao.readScene(context, sceneKey);
		if(scene == null) {
			logger.severe("invalid scene " + sceneKey);
			context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
			return false;
		}
		
		// check the scene is not closed
		if(Boolean.TRUE.equals(scene.getClosed())) {
			logger.severe("Scene " + sceneKey + " is closed");
			context.addError(Errors.IMPOSSIBLE_CLOSED, sceneKey);
			return false;
		}

		// check the scene is not paused
		if(Boolean.TRUE.equals(scene.getPaused())) {
			logger.severe("Scene " + sceneKey + " is paused");
			context.addError(Errors.IMPOSSIBLE_PAUSED, sceneKey);
			return false;
		}

		// Check the client is up-to-date
		if(scene.getTimestamp().after(clientSceneTimestamp)) {
			logger.severe("client is out-of-date");
			context.addError(Errors.OUTOFDATE);
			return false;
		}
		
		// FIXME should iterate to deal with concurrent updates of scene
		final Message message = new Message(scene);
		message.setAuthor(character);
		message.setContent(content);
		message.setType(MessageType.ACTION.getCode());
		message.setAction(action.getCode());
		createMessage(context, scene, message);
		return true;
	}
	
	/**
	 * To post an action message by character.
	 * 
	 * @param context execution context.
	 * @param campaignId campaign's id.
	 * @param characterId character's id.
	 * @param clientSceneId scene where post message, used to check client is up to date.
	 * @param clientSceneTimestamp just to check if the client is up to date.
	 * @param dices the dices to roll, a Map<Sides, NumberOfDices>.
	 * @param content message content.
	 * @return <code>true</code> if ok.
	 */
	public boolean playerRollDices(Context context, 
			long campaignId, long characterId, 
			long clientSceneId, Date clientSceneTimestamp, 
			Map<Integer, Integer> dices, String content) {

		return playerRollDices(context, 
				createCharacterKey(campaignId, characterId), 
				createSceneKey(campaignId, clientSceneId), 
				clientSceneTimestamp, dices, content);
	}
	
	
	/**
	 * To post an action message by character.
	 * 
	 * @param context execution context.
	 * @param characterKey character's key.
	 * @param clientSceneKey scene where post message, used to check client is up to date.
	 * @param clientSceneTimestamp just to check if the client is up to date.
	 * @param dices the dices to roll, a Map<Sides, NumberOfDices>.
	 * @param content message content.
	 * @return <code>true</code> if ok.
	 */
	public boolean playerRollDices(Context context, 
			Key characterKey, Key clientSceneKey, Date clientSceneTimestamp,
			Map<Integer, Integer> dices, String content) {

		if(dices == null || dices.isEmpty() 
				|| dices.containsKey(null) || dices.containsValue(null)
				|| characterKey == null || content == null 
				|| clientSceneKey == null || clientSceneTimestamp == null) {
			logger.severe("missing param");
			context.addError(Errors.REQUIRED);
			return false;
		}
	
		for(Map.Entry<Integer, Integer> entry : dices.entrySet()) {
			int sides = entry.getKey().intValue();
			if(sides <= 0) {
				logger.severe("invalid dice sides : " + sides);
				context.addError(Errors.ARGUMENT, "dice sides", sides);
				return false;
			}
			
			int number = entry.getValue().intValue();
			if(number <= 0 || number > 100) {
				logger.severe("invalid dice number : " + number);
				context.addError(Errors.ARGUMENT, "dice number", number);
				return false;
			}
		}
		
		// check the character.
		final Data.Character character = dao.readCharacter(context, characterKey);
		if(character == null) {
			logger.severe("invalid character " + characterKey);
			context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
			return false;
		}
		
		// check character's lock
		if(Boolean.TRUE.equals(character.getLocked())) {
			logger.severe("Character " + characterKey + " is locked");
			context.addError(Errors.IMPOSSIBLE_LOCKED, characterKey);
			return false;
		}
		
		// check character is not dead
		if(Boolean.TRUE.equals(character.getDead())) {
			logger.severe("Character " + characterKey + " is dead");
			context.addError(Errors.IMPOSSIBLE_DEAD, characterKey);
			return false;
		}		
		// check user rights
		if(!isOwner(context, character)) {
			logger.severe(context.getUser().getKey() 
					+ " cannot post for " + characterKey);
			context.addError(Errors.USER_USURPATION_PC);
			return false;
		}

		// Get the scene.
		final Key sceneKey = character.getScene();
		
		if(sceneKey == null) {
			logger.severe(characterKey + " cannot post, it has no current scene");
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		// Check the client is up-to-date
		if(!sceneKey.equals(clientSceneKey)) {
			logger.severe("client is out-of-date");
			context.addError(Errors.OUTOFDATE);
			return false;
		}

		// check the scene
		final Scene scene = dao.readScene(context, sceneKey);
		if(scene == null) {
			logger.severe("invalid scene " + sceneKey);
			context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
			return false;
		}
		
		// check the scene is not closed
		if(Boolean.TRUE.equals(scene.getClosed())) {
			logger.severe("Scene " + sceneKey + " is closed");
			context.addError(Errors.IMPOSSIBLE_CLOSED, sceneKey);
			return false;
		}

		// check the scene is not paused
		if(Boolean.TRUE.equals(scene.getPaused())) {
			logger.severe("Scene " + sceneKey + " is paused");
			context.addError(Errors.IMPOSSIBLE_PAUSED, sceneKey);
			return false;
		}

		// Check the client is up-to-date
		if(scene.getTimestamp().after(clientSceneTimestamp)) {
			logger.severe("client is out-of-date");
			context.addError(Errors.OUTOFDATE);
			return false;
		}

		// roll the dices.
		final Random random = new Random();
		final List<String> dicesRolled = new ArrayList<String>();
		for(Map.Entry<Integer, Integer> entry : dices.entrySet()) {
			int sides = entry.getKey().intValue();
			int number = entry.getValue().intValue();
			for(int n = 0; n < number; n++) {
				int dice = random.nextInt(sides) + 1;
				dicesRolled.add(dice + "/" + sides);
			}
		}
		
		// FIXME should iterate to deal with concurrent updates of scene
		final Message message = new Message(scene);
		message.setAuthor(character);
		message.setContent(content);
		message.setType(MessageType.DICEROLL.getCode());
		message.setAction(null);
		message.setDices(dicesRolled);
		createMessage(context, scene, message);
		return true;
	}

	
	/**
	 * Post a Speech Message.
	 * @param sceneKey scene containing the message.
	 * @param characterKey character who posts the speech.
	 * @param text text of the speech.
	 * @return true if ok.
	 */
	public boolean postSpeech(Context context, Key sceneKey, Key characterKey, String text) {
		
		final Data.Character character = dao.readCharacter(context, characterKey);
		
		if(character == null) {
			return false;
		}
		
		return postSpeech(context, sceneKey, character, text);
	}

	/**
	 * Utility method to create a message in a scene.
	 * @param scene scene receiving the message.
	 * @param message message to add.
	 */
	private void createMessage(Context context, Scene scene, Message message) {
		
		message.setDate(new Date());
		
		final Key lastMessageKey = scene.getLastMessage();
		message.setPrevious(lastMessageKey);

		long index = getNewMessageIndex(null);

		Message lastMessage = null;
		if(lastMessageKey != null) {
			lastMessage = dao.readMessage(context, lastMessageKey);
			if(lastMessage != null) {
				if(lastMessage.getIndex() != null) {
					index = getNewMessageIndex(lastMessage.getIndex());
				}
				lastMessage.setNext(message.getKey());
			}
		}

		message.setIndex(index);
		dao.save(context, message);

		if(lastMessage != null) {
			// donne here to get the 'message' key after the save operation
			dao.save(context, lastMessage);
		}
		
		if(scene.getFirstMessage() == null) {
			scene.setFirstMessage(message.getKey());
		}
		scene.setLastMessage(message.getKey());
		dao.save(context, scene);
	}
	
	
	private long getNewMessageIndex(Long index) {
		return index == null ? 50 : (index + 50);
	}
	
	/**
	 * Method to post an OFF message to a scene.
	 * PC can only post to their current scene.
	 * NPC cannot post OFF.
	 * NPC cannot receive OFF.
	 * PC can only post OFF to GM (GM see all OFF).
	 * 
	 * @param sceneKey scene to attach to message.
	 * @param authorKey author of the message or null if GM.
	 * @param toKey recipient of the message when GM.
	 * @param text text of the OFF message.
	 * @return true if ok.
	 */
	public boolean postOFF(Context context, Key sceneKey, Key authorKey, 
			Key toKey, String text) {
		
		Data.Character to = null;
		if(toKey != null) {
			to =  dao.readCharacter(context, toKey);
			if(to == null) {
				return false;
			}
			
			// Cannot post OFF to NPC
			if(to.getOwner() == null) {
				toKey = null;
			}
		}

		if(authorKey == null) { // GM
			if(sceneKey == null) {
				if(to == null) {
					return false;
				}
				
				// use the recipient current scene.
				sceneKey = to.getScene();
				
				if(sceneKey == null) {
					return false;
				}
			}
			
			// GM cannot post OFF as a PC
			authorKey = null;
			
		} else {
			final Data.Character author = dao.readCharacter(context, authorKey);
			
			if(author == null) {
				return false;
			}
			
			// NPC cannot post OFF messages.
			if(author.getOwner() == null) {
				return false;
			}
			
			// PC can only post on current scene.
			sceneKey = author.getScene();
			
			// PC can only post to GM.
			toKey = null;
		}
		
		final Scene scene = dao.readScene(context, sceneKey);
		
		if(scene == null) {
			return false;
		}

		final Message message = new Message(scene);
		message.setAuthor(authorKey);
		if(toKey != null) {
			message.getNonNullTo().add(toKey);
		}
		message.setContent(text);
		message.setType(MessageType.OFF.getCode());
		createMessage(context, scene, message);
		
		return true;
	}
	
	/**
	 * Number of messages accepted for pagination.
	 * Note that scenes are always complete, it's an approximative max.
	 */
	public final static int PAGINATION_MESSAGES = 25;
	
	/**
	 * Gives the last messages of the campaign for a character.
	 * @param character for wich get the last messages. Must be one of the current user characters.
	 * @return the last messages of the characters. scenes are limited but complete.
	 */
	public List<SceneSDO> getMessages(
			Context context, long campaignId, long characterId,
			Long lastSceneId, Date timestamp
	) {

		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		final Data.Character character = dao.readCharacter(context, characterKey);

		// Check the character exists.
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
			return null;
		}

		// Check user is character's owner.
		final User user = context.getUser();
		if(!isOwner(context, character)) {
			context.addError(Errors.USER_USURPATION);
			logger.severe("User " + user.getKey() 
					+ " cannot read messages of " + characterKey);
			return null;
		}
		
		// the result
		final List<SceneSDO> scenes = new ArrayList<SceneSDO>();
		
		if(timestamp != null) {
			final Map<Key, SceneSDO> mapScenes = new HashMap<Key, SceneSDO>();
			
			// Load the scenes wich are newer (or modified after) than timestamp
			final List<Scene> scenesDB = 
				dao.asListOfScenes(context, dao.addSceneFilterOnTimestamp(
					dao.queryScene(campaignKey), FilterOperator.GREATER_THAN, timestamp), null);

			// register the scenes.
			if(scenesDB != null) {
				for(Scene sceneDB : scenesDB) {
					final SceneSDO sceneSDO = readSceneSDO(context, sceneDB, null, character);
					mapScenes.put(sceneDB.getKey(), sceneSDO);
				}
			}
			
			// search new messages.
			final List<Message> messages =
				dao.asListOfMessages(context, dao.addMessageFilterOnTimestamp(
					dao.queryMessage(campaignKey), FilterOperator.GREATER_THAN, timestamp), null);
			
			// keep only visible messages.
			keepOnlyVisibleMessages(messages, character.getKey(), true);
			
			// dispatch messages on scenes
			if(messages != null) {
				for(Message message : messages) {
					final Key sceneKey = message.getKey().getParent();
					
					SceneSDO sceneSDO = mapScenes.get(sceneKey);
					
					if(sceneSDO == null) {
						// scene was not already loaded, so ...
						final Scene scene = dao.readScene(context, sceneKey);
						if(scene == null) {
							logger.severe("Invalid scene " + sceneKey);
							continue;
						}
						sceneSDO = readSceneSDO(context, scene, null, character);
						mapScenes.put(sceneKey, sceneSDO);
					}

					// add the message to the scene.
					if(sceneSDO.getMessages() == null) {
						sceneSDO.setMessages(new ArrayList<Message>());
					}
					
					sceneSDO.getMessages().add(message);
				}
			}
			
			scenes.addAll(mapScenes.values());
			
		} else {
			final String characterIndex = String.valueOf(characterId);
			
			final Key sceneKey;
			if(lastSceneId == null) {
				// use current scene as start
				sceneKey = character.getScene();
			} else {
				// get the "last scene"'s previous scene
				final Key lastSceneKey =  Scene.createKey(campaignKey, lastSceneId);
				final Scene lastScene = dao.readScene(context, lastSceneKey);
				if(lastScene == null) {
					logger.severe("Invalid scene " + lastSceneKey);
					context.addError(Errors.NOT_FOUND_SCENE, lastSceneKey);
					return null;
				}
				if(!lastScene.getNonNullCharacters().contains(characterKey)) {
					logger.severe("LastScene " + lastSceneKey 
							+ " is not associated with " + characterKey);
					context.addError(Errors.IMPOSSIBLE);
					return null;
				}
				
				// get the previous scene "for the current character"
				sceneKey = lastScene.getPrevious(characterIndex);
			}

			if(sceneKey == null) {
				logger.severe("cannot find a starting scene for " + characterKey);
				context.addError(Errors.IMPOSSIBLE);
				return null;
			}
			
			// Load the scene.
			Scene scene = dao.readScene(context, sceneKey);
			if(scene == null) {
				logger.severe("Invalid scene " + sceneKey);
				context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
				return null;
			}
			
			// Iterate over the scenes to reach (if possible), 25 messages
			int count = 0;
			while(scene != null && count < PAGINATION_MESSAGES) {
				
				// select all the messages of the scene, always, and order them.
				final List<Message> messages = order(dao.asListOfMessages(
					context, dao.queryMessage(scene.getKey()), null), scene.getFirstMessage());
				
				// keep only visible messages
				keepOnlyVisibleMessages(messages, characterKey, false);

				// note : even scenes with 0 messages must be returned because off the intro !
				final SceneSDO sceneSDO = readSceneSDO(context, scene, messages, character);

				scenes.add(0, sceneSDO);
				
				if(messages != null) {
					count += messages.size();
				}
				
				if(count < PAGINATION_MESSAGES) {
					final Key previousSceneKey = scene.getPrevious(characterIndex);
					scene = previousSceneKey == null ? null : dao.readScene(context, previousSceneKey);
				}
			}
		}
		
		return scenes.isEmpty() ? null : scenes;
	}
	
	/**
	 * Utility method to not completely load a SceneSDO. 
	 * 
	 * @param context execution context.
	 * @param scene scene to use to build the SDO.
	 * @param byCharacter character to view the scene.
	 * @return the {@link SceneSDO}.
	 */
	private SceneSDO readSceneSDO(Context context, Scene scene, 
		List<Message> messages, Data.Character character) {

		final SceneSDO sceneSDO = new SceneSDO();
		sceneSDO.setScene(scene);

		// Note : alias will be used after
		sceneSDO.setCharacters(dao.readCharacters(context, scene.getCharacters()));
		
		// keep only visible messages
		if(messages != null) {
			if(character != null) {
				keepOnlyVisibleMessages(messages, character.getKey(), false);
			}
			sceneSDO.setMessages(messages);
		}
		
		return sceneSDO;
	}
	
	private void keepOnlyVisibleMessages(List<Message> messages, Key characterKey, boolean keepDeleted) {
		
		if(messages == null) {
			return;
		}
		
		final Iterator<Message> imessages = messages.iterator();
		while(imessages.hasNext()) {
			final Message message = imessages.next();
			if(message != null) {
				if(!keepDeleted && Boolean.TRUE.equals(message.getDeleted())) {
					imessages.remove();
				} else if(MessageType.ACTION.getCode().equals(message.getType())) {
					// all actions of everyone are always seen
				} else if (MessageType.OFF.getCode().equals(message.getType())) {
					// player cans see OFF of GM (to him or public) and its own
					if(!(characterKey.equals(message.getAuthor()) 
							|| isPublicGMOFF(message) 
							|| message.getNonNullTo().contains(characterKey))) {
						imessages.remove();
					}
				} else if (MessageType.DICEROLL.getCode().equals(message.getType())) {
					// character can see its own dices, that's all.
					if(!characterKey.equals(message.getAuthor())) {
						imessages.remove();
					}
				}
			} else {
				imessages.remove();
			}
		}
	}
	
	
	private boolean isPublicOFF(Message message) {
		return message.getTo() == null || message.getTo().isEmpty();
	}
	
	private boolean isPublicGMOFF(Message message) {
		return isPublicOFF(message) && message.getAuthor() == null;
	}
	
	/**
	 * FIXME use cache
	 */
	public List<Data.Character> getSceneCharacters(Context context, Key sceneKey) {

		// query on campaign
		final Query query = dao.queryCharacter(sceneKey.getParent());
		
		// filter on scene
		dao.addCharacterFilterOnScene(query, FilterOperator.EQUAL, sceneKey);

		// no cursor ...
		return dao.asListOfCharacters(context, query, null);
	}
	/**
	 * To post an action message by character.
	 * 
	 * @param context execution context.
	 * @param campaignId campaign's id.
	 * @param characterId character's id.
	 * @param clientSceneId scene where post message, used to check client is up to date.
	 * @param clientSceneTimestamp just to check if the client is up to date.
	 * @param content OFF content.
	 * @return <code>true</code> if ok.
	 */
	public boolean playerPostOFF(Context context, 
			long campaignId, long characterId, long clientSceneId, 
			Date clientSceneTimestamp, String content) {
		
		return playerPostOFF(context, 
				createCharacterKey(campaignId, characterId), 
				createSceneKey(campaignId, clientSceneId), 
				clientSceneTimestamp, content);
	}

	/**
	 * To post an OFF message by character.
	 * 
	 * @param context execution context.
	 * @param characterKey character's key.
	 * @param clientSceneKey scene where post message, used to check client is up to date.
	 * @param clientSceneTimestamp just to check if the client is up to date.
	 * @param content message content.
	 * @return <code>true</code> if ok.
	 */
	public boolean playerPostOFF(Context context, 
			Key characterKey, Key clientSceneKey, Date clientSceneTimestamp,
			String content) {

		if(characterKey == null || content == null 
				|| clientSceneKey == null || clientSceneTimestamp == null) {
			logger.severe("missing param");
			context.addError(Errors.REQUIRED);
			return false;
		}
		
		// check the character.
		final Data.Character character = dao.readCharacter(context, characterKey);
		if(character == null) {
			logger.severe("invalid character " + characterKey);
			context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
			return false;
		}
		
		// check character's lock
		if(Boolean.TRUE.equals(character.getLocked())) {
			logger.severe("Character " + characterKey + " is locked");
			context.addError(Errors.IMPOSSIBLE_LOCKED, characterKey);
			return false;
		}
		
		// check character is not dead
		if(Boolean.TRUE.equals(character.getDead())) {
			logger.severe("Character " + characterKey + " is dead");
			context.addError(Errors.IMPOSSIBLE_DEAD, characterKey);
			return false;
		}		
		// check user rights
		if(!isOwner(context, character)) {
			logger.severe(context.getUser().getKey() 
					+ " cannot post for " + characterKey);
			context.addError(Errors.USER_USURPATION_PC);
			return false;
		}

		// Get the scene.
		final Key sceneKey = character.getScene();
		
		if(sceneKey == null) {
			logger.severe(characterKey + " cannot post, it has no current scene");
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		// Check the client is up-to-date
		if(!sceneKey.equals(clientSceneKey)) {
			logger.severe("client is out-of-date");
			context.addError(Errors.OUTOFDATE);
			return false;
		}

		// check the scene
		final Scene scene = dao.readScene(context, sceneKey);
		if(scene == null) {
			logger.severe("invalid scene " + sceneKey);
			context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
			return false;
		}
		
		// check the scene is not closed
		if(Boolean.TRUE.equals(scene.getClosed())) {
			logger.severe("Scene " + sceneKey + " is closed");
			context.addError(Errors.IMPOSSIBLE_CLOSED, sceneKey);
			return false;
		}

		// check the scene is not paused
		if(Boolean.TRUE.equals(scene.getPaused())) {
			logger.severe("Scene " + sceneKey + " is paused");
			context.addError(Errors.IMPOSSIBLE_PAUSED, sceneKey);
			return false;
		}

		// Check the client is up-to-date
		if(scene.getTimestamp().after(clientSceneTimestamp)) {
			logger.severe("client is out-of-date");
			context.addError(Errors.OUTOFDATE);
			return false;
		}
		
		// FIXME should iterate to deal with concurrent updates of scene
		final Message message = new Message(scene);
		message.setAuthor(character);
		message.setContent(content);
		message.setType(MessageType.OFF.getCode());
		message.setAction(null);
		createMessage(context, scene, message);
		return true;
	}
	
	/**
	 * Orders a list of messages.
	 * 
	 * @param messages messages to order
	 * @param firstKey known first element (see Scene#firstMessage)
	 * @return messages ordered.
	 */
	private List<Message> order(Collection<Message> messages, Key firstKey) {

		if(messages == null || messages.isEmpty())  {
			return null;
		}
		
		final Map<Key, Message> map = Data.map(messages);
		final List<Message> ordered = new ArrayList<Message>();
		Message previous = map.get(firstKey);
		while(previous != null) {
			ordered.add(previous);
			previous = map.get(previous.getNext());
		}
		
		return ordered.isEmpty() ? null : ordered;
	}
	
	/**
	 * 
	 * @param campaignId
	 * @param sceneId
	 * @param messageId
	 * @return
	 */
	public boolean deleteMessage(Context context, long campaignId, long sceneId, long messageId) {

		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Campaign campaign = dao.readCampaign(context, campaignKey);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignKey);
			context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
			return false;
		}
		
		final Key sceneKey = Scene.createKey(campaignKey, sceneId);
		final Key messageKey = Message.createKey(sceneKey, messageId);

		if(!isGameMaster(context, campaign)) {
			logger.severe("User " + context.getUser().getKey() 
					+ " cannot delete " + messageKey);
			context.addError(Errors.USER_USURPATION_GM);
			return false;
		}
		
		final Message message = dao.readMessage(context, messageKey);
		if(message == null) {
			logger.severe("Message not found " + messageKey);
			context.addError(Errors.NOT_FOUND_MESSAGE, messageKey);
			return false;
		}
		
		if(Boolean.TRUE.equals(message.getDeleted())) {
			logger.warning("Message already deleted " + messageKey);
			return true;
		}
		
		message.setDeleted(Boolean.TRUE);
		dao.save(context, message);
		logger.info("message " + messageKey + " deleted");
		
		return true;
	}
	
	public boolean justForTestsMakeThemTalkRandomly(Context context, long campaignId, long characterId) {

		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		final Data.Character character = dao.readCharacter(context, characterKey);

		// Check the character exists.
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			return false;
		}

		final Key sceneKey = character.getScene();
		final Scene scene = dao.readScene(context, sceneKey);
		
		// random a number of messages to write.
		final Random random = new Random();
		final int countMessages = random.nextInt(4);
		logger.info("random " + countMessages + " to write randomly");
		
		for(int n = 0; n < countMessages; n++) {
			
			final List<Key> characters = new ArrayList<Key>(scene.getNonNullCharacters());

			// Remove current character.
			while(characters.remove(characterKey));
			if(characters.isEmpty()) {
				return false;
			}

			final Key speekerKey = characters.get(random.nextInt(characters.size()));
			
			postSpeech(context, sceneKey, speekerKey, "This a randomly generated message from " + speekerKey);
		}
		
		return true;
	}

	public boolean justForTestsMakeThemRollDicesRandomly(Context context, long campaignId, long characterId) {

		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		final Data.Character character = dao.readCharacter(context, characterKey);

		// Check the character exists.
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			return false;
		}

		final Key sceneKey = character.getScene();
		final Scene scene = dao.readScene(context, sceneKey);
		
		// random a number of messages to write.
		final Random random = new Random();
		
		final List<Key> charactersKeys = new ArrayList<Key>(scene.getNonNullCharacters());

		// Remove current character.
		while(charactersKeys.remove(characterKey));
		if(charactersKeys.isEmpty()) {
			return false;
		}

		List<Data.Character> characters = dao.read(context, Data.Character.class, charactersKeys);
		characters = removeNPCs(characters);

		if(characters.isEmpty()) {
			return false;
		}
		
		final Data.Character roller = characters.get(random.nextInt(characters.size()));

		final User rollerOwner = dao.readUser(context, roller.getOwner());
		if(rollerOwner == null) {
			logger.severe("Owner not found " + roller.getOwner());
			return false;
		}
		
		final Map<Integer, Integer> dices = new HashMap<Integer, Integer>();
		final int types = random.nextInt(4) + 1;
		logger.info("Roll " + types + " types of dices");
		for(int n = 0; n < types; n++) {
			boolean ok = false;
			while(!ok) {
				final int sides = random.nextInt(100) + 1;
				if(!dices.containsKey(sides)) {
					final int number = random.nextInt(4) + 1;
					logger.info("Roll " + number + " dices of " + sides + " sides");
					dices.put(sides, number);
					ok = true;
				}
			}
		}
		
		return playerRollDices(new Context(context, rollerOwner), roller.getKey(), 
			sceneKey, scene.getTimestamp(), 
			dices, "A random dice roll !");
	}
	
	
	public boolean justForTestsCreateSequenceRandomly(Context context, long campaignId, long characterId) {
		
		final Key campaignKey = Data.Campaign.createKey(campaignId);

		// Get all playable characters.
		final List<Data.Character> characters = dao.getCampaignCharacters(context, campaignKey);

		if(characters == null || characters.isEmpty()) {
			return false;
		}
		
		// Remove current character.
		Data.Character character = null;
		final Iterator<Data.Character> icharacters = characters.iterator();
		while(icharacters.hasNext()) {
			final Data.Character next = icharacters.next();
			if(next.getKey().getId() == characterId) {
				character = next;
				icharacters.remove();
			}
		}
		if(characters.isEmpty() || character == null) {
			return false;
		}
		
		// Randomly remove guys to create a new scene with the remaining
		final Random random = new Random();
		final int countToRemove = random.nextInt(characters.size());
		for(int n = 0; n < countToRemove; n++) {
			characters.remove(random.nextInt(characters.size()));
		}

		characters.add(character);
		
		// Start the new scene ... and creates automatically the others ...
		final String introduction = "This a randomly generated scene, have fun ...";
		startScene(context, introduction, characters.toArray(new Data.Character[characters.size()]));
		
		return true;
	}
	
	public boolean justForTestsDeleteMessageRandomly(Context context, long campaignId, long characterId) {
		
		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		final Data.Character character = dao.readCharacter(context, characterKey);

		// Check the character exists.
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
			return false;
		}

		// choose a number of scene to fetch before deleting a message
		final Random random = new Random();
		final int countScenes = random.nextInt(5);

		Key sceneKey = character.getScene();
		Scene scene = null;
		for(int n = 0; n < countScenes; n++) {
			if(sceneKey != null) {
				scene = dao.readScene(context, sceneKey);
				sceneKey = scene.getPrevious(String.valueOf(characterId));
			}
		}

		if(scene == null) {
			logger.severe("was not able to find a scene where delete a message");
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		sceneKey = scene.getKey();
		
		final List<Message> messages = dao.getSceneMessages(context, sceneKey);
		
		if(messages == null || messages.isEmpty()) {
			logger.severe("no messages in scene " + sceneKey);
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		// choose randomly a message to delete
		final long messageId = messages.get(random.nextInt(messages.size())).getKey().getId();
		
		// check the campaign
		final Campaign campaign = dao.readCampaign(context, campaignKey);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignKey);
			context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
			return false;
		}
		
		// may be 100% of test cases
		if(!isGameMaster(context, campaign)) {
			// then we cheat ...
			final Key masterKey = campaign.getMaster();
			final User master = dao.readUser(context, masterKey);
			if(master == null){
				logger.severe("Master not found " + masterKey);
				context.addError(Errors.NOT_FOUND_USER, masterKey);
				return false;
			}
			context = new Context(context, master);
		}
		
		return deleteMessage(context, campaignId, sceneKey.getId(), messageId);
	}

	public boolean justForTestsReindexRandomly(Context context, long campaignId, long characterId) {
		
		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		final Data.Character character = dao.readCharacter(context, characterKey);

		// Check the character exists.
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
			return false;
		}

		// choose a number of scene to fetch before deleting a message
		final Random random = new Random();
		final int countScenes = random.nextInt(5);

		Key sceneKey = character.getScene();
		Scene scene = null;
		for(int n = 0; n < countScenes; n++) {
			if(sceneKey != null) {
				scene = dao.readScene(context, sceneKey);
				sceneKey = scene.getPrevious(String.valueOf(characterId));
			}
		}

		if(scene == null) {
			logger.severe("was not able to find a scene where reindex messages");
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		sceneKey = scene.getKey();
		
		final List<Message> messages = dao.getSceneMessages(context, sceneKey);
		
		if(messages == null || messages.size() < 2) {
			logger.severe("no enough messages in scene " + sceneKey);
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		// How messages will be reindexed
		final int number = random.nextInt(messages.size()) + 1;
		final int max = (messages.size() + 2) * 50;

		final List<Message> messages2 = new ArrayList<Message>(messages);
		
		for(int n = 0; n < number; n++) {
			final Message message = messages2.get(random.nextInt(messages2.size()));

			// Random a none used index
			long index = random.nextInt(max);
			boolean ok = false;
			rerandom: while(!ok) {
				ok = true;
				for(Message m : messages) {
					if(m.getIndex() == index) {
						ok = false;
						index = random.nextInt(max);
						continue rerandom;
					}
				}
			}
			
			logger.info("reindex " + message.getKey() + " from " + message.getIndex() + " to " + index);
			message.setIndex(index);
			dao.save(context, message);
			
			messages2.remove(message);
		}
		
		return true;
	}

	public boolean justForTestsInsertOFFRandomly(Context context, long campaignId, long characterId) {
		
		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		final Data.Character character = dao.readCharacter(context, characterKey);

		// Check the character exists.
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
			return false;
		}

		// choose a number of scene to fetch before deleting a message
		final Random random = new Random();
		final int countScenes = random.nextInt(5);

		Key sceneKey = character.getScene();
		Scene scene = null;
		for(int n = 0; n < countScenes; n++) {
			if(sceneKey != null) {
				scene = dao.readScene(context, sceneKey);
				sceneKey = scene.getPrevious(String.valueOf(characterId));
			}
		}

		if(scene == null) {
			logger.severe("was not able to find a scene where insert a message");
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		sceneKey = scene.getKey();
		
		final List<Message> messages = dao.getSceneMessages(context, sceneKey);
		
		if(messages == null || messages.isEmpty()) {
			logger.severe("no messages in scene " + sceneKey);
			context.addError(Errors.IMPOSSIBLE);
			return false;
		}

		// choose randomly after which insert a new message
		final long messageId = messages.get(random.nextInt(messages.size())).getKey().getId();
		
		
		// check the campaign
		final Campaign campaign = dao.readCampaign(context, campaignKey);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignKey);
			context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
			return false;
		}

		// may be 100% of test cases
		if(!isGameMaster(context, campaign)) {
			// then we cheat ...
			final Key masterKey = campaign.getMaster();
			final User master = dao.readUser(context, masterKey);
			if(master == null){
				logger.severe("Master not found " + masterKey);
				context.addError(Errors.NOT_FOUND_USER, masterKey);
				return false;
			}
			context = new Context(context, master);
		}
		
		return insertMessage(context, campaignId, sceneKey.getId(), messageId, 
				"This is a generated randomly inserted message");
	}

	/**
	 * Temporary method to insert a public OFF.
	 */
	public boolean insertMessage(
			Context context, long campaignId, long sceneId, 
			long messageBeforeId, String content) {
		
		if(content == null) {
			context.addError(Errors.REQUIRED, "content");
			return false;
		}
		
		// check the campaign
		final Key campaignKey = Campaign.createKey(campaignId);
		final Campaign campaign = dao.readCampaign(context, campaignKey);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignKey);
			context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
			return false;
		}
		
		// check user rights
		if(!isGameMaster(context, campaign)) {
			logger.severe("User " + context.getUser().getKey() 
					+ " cannot insert messages in " + campaignKey);
			context.addError(Errors.USER_USURPATION);
			return false;
		}
		
		// find the message before
		final Key sceneKey = Scene.createKey(campaignKey, sceneId);
		final Scene scene = dao.readScene(context, sceneKey);
		if(scene == null) {
			logger.severe("Scene not found " + sceneKey);
			context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
			return false;
		}

		final Key messageBeforeKey = Message.createKey(sceneKey, messageBeforeId);
		final Message messageBefore = dao.readMessage(context, messageBeforeKey);
		if(messageBefore == null) {
			logger.severe("Message not found " + messageBeforeKey);
			context.addError(Errors.NOT_FOUND_MESSAGE, messageBeforeKey);
			return false;
		}

		final Transaction tx = dao.beginTransaction();
		try {
			final long newIndex;
			
			// find the message after
			final Key messageAfterKey = messageBefore.getNext();
			final Message messageAfter;
			if(messageAfterKey != null) {
				messageAfter = dao.readMessage(context, messageAfterKey);
				if(messageAfter == null) {
					logger.severe("Message not found " + messageAfterKey);
					context.addError(Errors.NOT_FOUND_MESSAGE, messageAfterKey);
					return false;
				}
				
				newIndex = (messageBefore.getIndex() + messageAfter.getIndex()) / 2;
				
				if(newIndex == messageBefore.getIndex() 
						|| newIndex == messageAfter.getIndex()) {
					logger.severe("No more indexes to insert between " 
						+ messageBeforeKey + " and " + messageAfterKey);
					context.addError(Errors.IMPOSSIBLE, "index");
					return false;
				}

			} else {
				newIndex = getNewMessageIndex(messageBefore.getIndex());
				messageAfter = null;
			}

			final Message message = new Message(scene);
			message.setDate(new Date());
			message.setType(MessageType.OFF.getCode());
			message.setPrevious(messageBeforeKey);
			message.setContent(content);
			message.setIndex(newIndex);
			if(messageAfter != null) {
				message.setNext(messageAfterKey);
			}
			dao.save(context, message);
			
			messageBefore.setNext(message);
			dao.save(context, messageBefore);
			
			if(messageAfter != null) {
				messageAfter.setPrevious(message);
				dao.save(context, messageAfter);
			} else {
				scene.setLastMessage(message);
				dao.save(context, scene);
			}

			tx.commit();

			logger.info("Message" + message.getKey() + " inserted after " 
				+ messageBeforeKey + " and before " + messageAfterKey 
				+ " with index " + newIndex);

			return true;
			
		} finally {
			if(tx.isActive()) {
				tx.rollback();
			}
		}
		
	}
	
}
